home *** CD-ROM | disk | FTP | other *** search
/ Developer CD Series 1997 May: Tool Chest / Dev.CD May 97 TC.toast / Sample Code / Development Tools & Languages / AppsToGo / DTS.Lib / =Using TreeObj.c < prev    next >
Encoding:
Text File  |  1994-09-22  |  38.8 KB  |  728 lines  |  [TEXT/MPS ]

  1. ***** TreeObj.c usage documentation *****/
  2.  
  3. Purpose:  To simplify and standardize document manipulation, including undo.
  4.  
  5. Defining a generic document architecture that is flexible enough to handle the
  6. many and varied document types, yet simple enough to justify using it, can be
  7. difficult.
  8.  
  9. The document structure type chosen here is a simple hierarchy.  For simple document
  10. types, the hierarchical feature can simply be ignored.  All of the member objects of
  11. the document can simply be children of the root object.  This gives the document a
  12. linear feel, (all objects are at the same level in the hierarchy), which is all that
  13. is needed for a lot of applications.
  14.  
  15. For more complex document structures that have many data types which inter-relate, the
  16. hierarchical model is quite useful.  The sample given here is a draw program.  The draw
  17. example demonstrates both the linear document type (no grouping used) and the hierarchical 
  18. document type (grouping used).  If no grouping is used, then all of the objects added to
  19. the document are children of the root object.  The objects are arranged linearly off the
  20. root document object.  If some of the objects are grouped, a group object is first added
  21. to the root object, and then the selected objects are moved so that they are children of
  22. the group object.  Now the document is no longer linear.  The root object has children,
  23. some of which may be group objects.  These group objects in turn have children.
  24.  
  25. This sort of hierarchical support makes grouping of objects in a draw-type program trivial.
  26. It naturally represents the organization of the objects within the document.
  27.  
  28. An additional benefit of managing the objects in a document in such a standardized way is
  29. that additional application features such as undo/redo can be automatically supported.
  30. Whenever a change is to be made to the document, one of a few document-hierarchy-management
  31. calls is made first.  The different operations that can be done to the hierarchical document
  32. are:
  33.  
  34.     NewChild();
  35.     DisposeChild();
  36.     CopyChild();
  37.     MoveChild();
  38.     ModifyChild();
  39.     SwapChildren();
  40.  
  41. These six operations cover the various operations that can be performed to one or more of
  42. the document objects.  Children can be added, removed, copied, moved, modified, or swapped.
  43. If these calls are used to modify objects within the document hierarchy, then the editing
  44. operations done to the document can automatically be recorded for undo/redo purposes.
  45.  
  46. Let's look at a sample NewChild() call and its prototype:
  47.  
  48. TreeObjHndl    NewChild(short editType, TreeObjHndl parentHndl, short childNum, short ctype, long size);
  49.  
  50. NewChild(), if successful, returns a handle for the child object added to the document.
  51. The parameters are:
  52.     editType:    Application edit type for which this document modification is being done.
  53.     parentHndl:    The child created will be added as a child to this object.
  54.     childNum:    The child will be added to the parent as this child number.
  55.     ctype:        The child will be of this type.
  56.     size:        The child will be created this initial size (if size is greater than minimum).
  57.  
  58. NewChild() must have a parent declared.  This begs the question of where the initial parent
  59. comes from.  At some point, NewRootObj() must be called to create the initial root parent.
  60. As would be expected, the root object has no parent.  Once there is an initial root object,
  61. children can then be added to the document via NewChild().
  62.  
  63. The editType of the NewChild() call is used for automatically tracking undo/redo.  Many
  64. individual operations may be done to the document that are all for the same editing operation.
  65. Let's say that you have a word-processor application that has paragraph objects.  If your
  66. document has 27 paragraphs, then you have 27 children off the root object.  The order of the
  67. children is the order of the paragraphs in your document.
  68.  
  69. Let's now say that the user has selected some text.  The text selected starts in the middle of
  70. paragraph 3, goes through paragraphs 4-5, and through half of the text in paragraph 6.
  71. The user then deletes this text.  We may have declared this type of edit to be of type
  72. DELETE_EDIT.  The edits to the document that we will perform are:
  73.  
  74. 1) Modify paragraph 3 by removing the selected text and keeping the unselected text.
  75. 2) Delete paragraphs 4-5.
  76. 3) Modify paragraph 6 by removing the selected text and keeping the unselected text.
  77.  
  78. For step 1, we would first use ModifyChild().  The prototype for ModifyChild() is:
  79.  
  80.     OSErr    ModifyChild(short editType, TreeObjHndl phndl, short childNum, Boolean deepCopy);
  81.  
  82. ModifyChild() makes a copy of the object and places the copy in the undo hierarchy.  We are
  83. now free to modify the object any way we see fit.  If the user chooses to undo the change,
  84. the undo code simply swaps the data of the modified object with the copy that was saved in
  85. the undo hierarchy.  If the user later chooses to redo the edit, the undo code simply swaps
  86. the data again to perform the redo.
  87.  
  88. NOTE:  ModifyChild() may fail, since a copy of the object is made.  There may not be enough
  89. memory for this copy to be created.  If this is so, then a memFullErr will be returned.
  90.  
  91. The parameters are:
  92.     editType:    Application edit type for which this document modification is being done.
  93.     parentHndl:    Parent of the child being modified.
  94.     childNum:    Child number (of the parent) being modified.
  95.     deepCopy:    If true, the children of this child are also copied into the undo hierarchy.
  96.  
  97. The editType parameter is used to determine if this document modification is of the same
  98. edit type as previous document modifications.  If it is, then the operation is grouped
  99. with the other operations.  This is all done for the purposes of undo/redo.  Multiple objects
  100. may be modified in a single edit, and these modifications all have to be undone/redone with
  101. a single undo/redo.
  102.  
  103. If the editType is different than the last document modification, then a new undo group is
  104. started, and this operation is recorded in this new group.
  105.  
  106. For the above example, we would make the following call to ModifyChild() prior to removing
  107. the selected text from paragraph 3:
  108.  
  109.     err = ModifyChild(DELETE_EDIT, root, 3, false);
  110.  
  111. Of course, it would be a really odd application in which the '3' was hard-coded as in
  112. the above line.
  113.  
  114. Now paragraphs 4 and 5 need to be deleted.  This is done by using DisposeChild().
  115. The prototype for DisposeChild() is:
  116.     void    DisposeChild(short editType, TreeObjHndl parentHndl, short childNum);
  117.  
  118. Note that DisposeChild() will succeed, as it doesn't have to create a new handle for the
  119. copy as ModifyChild() does.  HOWEVER:  DisposeChild doesn't necessarily free up ram, as
  120. you would expect.  All that occurs is that the child is moved into the undo hierarchy so
  121. that undo can move the child back into the document.
  122.  
  123. To delete children 4 and 5, we would do the following:
  124.  
  125.     DisposeChild(DELETE_EDIT, root, 4);
  126.     DisposeChild(DELETE_EDIT, root, 4);
  127.  
  128. Note that for both calls we dispose of child number 4.  This is because once the first child
  129. is disposed of, what used to be child 5 is now child 4.
  130.  
  131. The last operation we would do is to declare our intent to change to the end child in the
  132. selection range.  This would be done as follows:
  133.     err = ModifyChild(DELETE_EDIT, root, 4, false);
  134.  
  135. The delete (assuming no errors) has now been completed.  Actually, the delete has occurred
  136. in either case.  If there were errors, all that occurred is that the undo information wasn't
  137. recorded.
  138.  
  139. Now let's say that the user selects a new range of text and deletes it, as well.  We would
  140. do the same thing as above to delete the text.  Here's the problem:  The editType of this
  141. second text delete is the same as the first.  Both document edits were of type DELETE_EDIT.
  142. This means that both operations were recorded under the same undo task.  When the user
  143. chooses undo, both deletes will be undone.  This isn't what we want.
  144.  
  145. To fix this problem, we have to make one additional call.  We need to call NewUndo().
  146. NewUndo() closes out the old editing task.  When you then make some document modification,
  147. it will be recorded in a separate undo task.
  148.  
  149. NewUndo() only takes a single parameter.  Just pass it any handle in the document.  Any
  150. object in the document can serve as a reference to the document.
  151.  
  152.  
  153. An object handle has 3 parts:
  154. 1) The header information.
  155. 2) The application-defined object data.
  156. 3) The child handle table.
  157.  
  158.  
  159. The object header is of the following form:
  160.  
  161. typedef struct TreeObj {
  162.     short        type;
  163.     short        numChildren;
  164.     long        dataSize;
  165.     long        treeID;
  166.     TreeObjHndl    parent;
  167. } TreeObj;
  168.  
  169. These fields are automatically filled out whenever an object is created via NewRootObj()
  170. or NewChild().
  171.  
  172. The application-defined data area is automatically initialized with 0's.  When NewRootObj()
  173. or NewChild() is called, a size parameter is passed in.  There is also a table of minimum sizes
  174. for each object type that needs to be defined.  If the size passed in is smaller than the
  175. minimum size, then the minimum size is used as the data size instead.
  176.  
  177. The minimum size table is a global array of longs called gMinTreeObjSize.  Its definition is
  178. found in File.c.  For each type of object defined, there needs to be a minimum size defined.
  179.  
  180. There are some predefined object types.  These are defined by the following:
  181.  
  182. #define ROOTOBJ     1
  183. #define UNDOOBJ     2
  184. #define UNDOTASKOBJ 3
  185. #define UNDOPARTOBJ 4
  186. #define WFMTOBJ     5
  187. #define CTLOBJ      6
  188.  
  189. These are the mandatory object types for supporting this implementation of the document
  190. structure and undo/redo features.
  191.  
  192. Application-defined object types can start with type number 16.  Type number 0 is reserved
  193. and is currently undefined.  Types 7-15 are reserved for future objects that may be added
  194. to the DTS.Lib application framework.
  195.  
  196. The numChildren field is initially 0 after calling either NewRootObj() or NewChild().
  197.  
  198. The dataSize is the effective data size after object creation, which is either the size passed
  199. in, or the minimum object size, whichever is greater.
  200.  
  201. The treeID field is initially set to 0 by both NewRootObj() and NewChild().  This field is
  202. used to uniquely identify members of the hierarchy by ID.  (More on this later.)
  203.  
  204. The parent field is a reference to the handle which is the parent.  As you would expect,
  205. objects created by NewRootObj() have no parent, and therefore this field is nil.
  206.  
  207.  
  208. The application-defined data area starts immediately after the header information.  As the
  209. structure of this portion is application-defined and variant, there is no structure pre-defined
  210. for it.  Each object type will have a unique structure definition for this portion of the object.
  211.  
  212.  
  213. The final part of an object is the child handle table.  The child handle table starts
  214. immediately after the data portion of the object.  The size of the header structure plus
  215. the value in the dataSize field serves as an offset from the beginning of the object to
  216. the child handle table.  Given the relationship between dataSize and the child handle
  217. table, it is important that the object isn't resized directly.  There are calls for managing
  218. the data area and its size that take the child handle table into account.  These are:
  219.  
  220.     OSErr    AdjustDataSize(TreeObjHndl hndl, long delta);
  221.     OSErr    SetDataSize(TreeObjHndl hndl, long newSize);
  222.     OSErr    SlideData(TreeObjHndl hndl, long offset, long delta);
  223.     void    *GetDataPtr(TreeObjHndl hndl);
  224.  
  225. AdjustDataSize() alters the size of the data area, based on the delta passed in.  If you wish
  226. to decrease the data area by one byte, for example, pass in a -1.  AdjustDataSize() does not
  227. reposition any of the data within the data area.  It does move the child handle table so that
  228. it continues to be positioned immediately after the data area.
  229.  
  230. SetDataSize() does as expected.  It specifically sets the data size to the designated
  231. size.  It also does not shift the data, but does do the appropriate child handle table
  232. maintenance, just as AdjustDataSize() does.
  233.  
  234. SlideData() operates the same as AdjustDataSize(), plus it also slides some or all of the
  235. data in the data area.  Use SlideData() to insert or remove data at some location within
  236. the data area.
  237.  
  238. GetDataPtr() simply returns a pointer to the beginning of the data area.  IT DOES NOT LOCK
  239. THE HANDLE!!  If the handle needs locking, then you must do this yourself.  Since the object
  240. is simply a handle, this poses no difficulties.  (Unlock when you are done, of course.)
  241.  
  242.  
  243. The child handle table is automatically handled.  There shouldn't be a reason that you need
  244. to manage or reference this yourself.  If however you end up needing to do this, there
  245. are calls to manage the child handle table.
  246.  
  247.  
  248. Objects referencing other objects:
  249.  
  250. Let's revisit the word-processor example.  Once again, we have this paragraph-based word
  251. processor.  The user selects some text.  The cursor location (insertion point) must be kept.
  252. A reference to the starting object and an offset into the data of that object would serve as
  253. the cursor location reference.
  254.  
  255. This reference to the paragraph object containing the insertion point could be kept as either
  256. a handle reference or a child number reference.  Either would serve the purpose.  Both would
  257. uniquely describe an object in the document.  The handle reference would directly reference
  258. the object.  The child number reference would indirectly reference the object.  If we used
  259. child numbers as our reference and we desired to get the handle of the object, we would do
  260. something like the following:
  261.  
  262.     chndl = GetChildHndl(root, cnum);
  263.  
  264. In our document example, all of the paragraph objects are children of the root object.
  265. In this case it would be a simple matter of getting the child handle.  The child number
  266. reference would probably be stored in the root object, as it is a global piece of information
  267. for this document.  We would probably want it saved with the document, which would occur
  268. automatically if it were stored in the root object.
  269.  
  270. So, since they both seem to work, which is better?  Is keeping the handle better, or is keeping
  271. the child number better?  When saving a document, child numbers will be meaningful when the
  272. document is opened at a future date.  References to handles aren't meaningful when saved to disk
  273. and then reloaded.  For this reason it seems that child number should win out.
  274.  
  275. But what if you wish to keep a reference to some arbitrary point in the document?  What if you
  276. don't know what the parent of that object is?  What if the object is at some arbitrary depth
  277. in the hierarchy?  In this case, it seems pretty clear that we wish to use handle references
  278. instead of child number references, at least when the document is in memory.
  279.  
  280. So what about handle references when saved to disk?  We need these references to persist.  To
  281. do this, we must convert them to something that saves meaningfully and can be converted back
  282. to handle references when the document is reopened.
  283.  
  284. This is what the treeID field in the object header is for.  When DoNumberTree() is called,
  285. all of the objects in the hierarchy are uniquely numbered.  DoNumberTree() is automatically
  286. called by the shell just prior to the objects in a hierarchy are written to disk.  In
  287. addition, prior to writing an object out to disk, the object is called with a message
  288. requesting it to convert any handle references it contains to treeID references.  For each
  289. handle reference that needs to be converted, you need to call Hndl2ID() to do the conversion.
  290. Hndl2ID() depends on DoNumberTree() already being called so that all of the treeID fields
  291. for all of the objects in the hierarchy are current.
  292.  
  293. After calling the object with the handle-to-id conversion message, the object is written
  294. to disk.  Once it is written, a message is sent to the object requesting it to covert the
  295. id back into a handle reference.  For each reference converted, you need to call ID2Hndl()
  296. to deconvert the reference back into a handle.
  297.  
  298. When a document is opened, all of the objects are first read into memory.  Once the entire
  299. document is in memory, DoNumberTree() is called, and then each object in the document is
  300. sent a message requesting it to convert the converted handle references back into real
  301. handle references.
  302.  
  303. The reason that the entire document must first be read is that the reference to another object
  304. may be to an object that is later in the document.  Only after the entire document is read in
  305. is it possible to resolve all references to anywhere in the document.
  306.  
  307. This messaging mechanism allows you to use handle references within your application without
  308. worrying about them persisting through a save/open cycle.  So, given the above messaging
  309. mechanism, using either child number references or handle references is equally valid.
  310. Whatever seems easiest for a particular application is the one to use.
  311.  
  312.  
  313. There is a standard set of messages passed to objects.  The above message is just one of these.
  314. The standard list of messages (and possible sub-messages) is defined as below:
  315.  
  316. #define INITMESSAGE      0        /* Additional object initialization. */
  317. #define     CREATEINIT       0        /* Object being initially created. */
  318. #define     WINDOWINIT       1        /* Document/object being assigned to a window. */
  319. #define     NOWINDOWINIT     2        /* Document/object being freed from a window. */
  320.  
  321. #define FREEMESSAGE      1        /* Additional object disposal. */
  322.  
  323. #define COPYMESSAGE      2        /* Additional object cloning. */
  324.  
  325. #define UNDOMESSAGE      3        /* Additional object undo. */
  326. #define     UNDOFROMDOC      0        /* Object leaving document. */
  327. #define     UNDOTODOC        1        /* Object returning to document. */
  328.  
  329. #define CONVERTMESSAGE   4        /* Hndl2ID or ID2Hndl conversions. */
  330. #define     CONVERTTOID      0        /* Convert handle references to ID references. */
  331. #define     CONVERTTOHNDL    1        /* Convert ID references to handle references. */
  332.  
  333. #define FREADMESSAGE     5        /* Read object data from file. */
  334. #define FWRITEMESSAGE    6        /* Write object data to file. */
  335.  
  336. #define HREADMESSAGE     7        /* Read object data from handle. */
  337. #define HWRITEMESSAGE    8        /* Write object data to handle. */
  338.  
  339. #define HITTESTMESSAGE   9        /* Test if object was hit. */
  340. #define     HITTESTOBJ       0        /* Test body of object for hit. */
  341. #define     HITTESTGRABBER   1        /* Test sizing parts for hit. */
  342. #define     CANBETARGET      2        /* Return true if object can become target. */
  343. #define     CANTAKEKEYS      3        /* Return true if target can take keys. */
  344.  
  345. #define GETRGNMESSAGE    10        /* Return object bounding box. */
  346. #define GETBBOXMESSAGE   11        /* Return object bounding box. */
  347. #define SETBBOXMESSAGE   12        /* Set object bounding box. */
  348. #define SECTBBOXMESSAGE  13        /* Check if rect intersects object bounding box. */
  349.  
  350. #define TARGETMESSAGE    14        /* Set target status for object. */
  351. #define     TARGETOFF        0        /* Make the object no longer the target. */
  352. #define     TARGETON         1        /* Make the object the target. */
  353.  
  354. #define DRAWMESSAGE      15        /* Draw some form of the object. */
  355. #define     DRAWOBJ          0        /* Draw the object. */
  356. #define     ERASEOBJ         1        /* Draw the object. */
  357. #define     DRAWSELECT       2        /* Draw the selection portion of the object. */
  358. #define     DRAWGHOST        3        /* Draw an xor-ghost (for dragging) of the image. */
  359. #define     DRAWMASK         4        /* Draw a mask of the image (for offscreen layer masking.) */
  360.  
  361. #define PRINTMESSAGE     16        /* Print the object. */
  362.  
  363. #define VHMESSAGE        17        /* Format View Hierarchy information for the object. */
  364.  
  365.  
  366. INITMESSAGE:  This is called with a sub-message of what kind of init it is.  The CREATEINIT
  367. sub-message  indicates that the object is initially being created.  This sub-message is in
  368. case there is additional data initialization that must occur.  Depending on the object, there
  369. may be handles that need to be created off the object itself.  It may not be a simple linear
  370. block of data.  In these cases, you want to do this additional initialization when the
  371. INITMESSAGE is received.
  372. The WINDOWINIT sub-message is in case certain objects have a different state when the document
  373. has a window than when it doesn't.  For example:  If there were a TextEdit object, then when
  374. the file is initially read in and the CREATEINIT sub-message is passed to the object, there is
  375. no window to pass to TENew() to create a new TextEdit record.  The data has to be read in as
  376. regular text that has no TERecord yet.  Once the document is assigned a window, a TERecord can
  377. be created for the object and the text data can be moved into the TERecord.  This additional
  378. WINDOWINIT sub-message is defined for just such objects that you may create.  Note that
  379. nowhere in the application framework is there a call that passes a WINDOWINIT sub-message.
  380. This would be an application-specific function, and therefore this call would belong in the
  381. application.  The most likely place for this would be in the InitContent() function for the
  382. application.  You would do something like the following:
  383.     DoFTreeMethod(root, INITMESSAGE, WINDOWINIT);
  384. This would pass each object in the document an INITMESSAGE with a sub-message of WINDOWINIT.
  385. The final sub-message is NOWINDOWINIT.  This message occurs when a document is being detached
  386. from a window.  If an object has to change state to accomodate being related to a window, then
  387. it would need to change state back when disassociated with that window.
  388.  
  389. FREEMESSAGE:  This is called due to DisposeChild() being called.  DisposeChild() will
  390. dispose of simple objects that don't have additional handles of data.  Any handles that were
  391. created at INITMESSAGE time should be disposed of when the FREEMESSAGE is received.
  392.  
  393. COPYMESSAGE:  When CopyChild() is called, a new child is created via calling NewChild().
  394. Normally when NewChild() is called, the object is called with INITMESSAGE.  However,
  395. when a child is being copied, the data area is copied into the copy child.  This data
  396. copy would clobber any handle references the newly created copy would have, thus orphaning
  397. those handles.  For this reason, no INITMESSAGE was passed to the object by NewChild().
  398. The data was copied into the copy however, so any handle references in the copy aren't unique
  399. references.  The original child has the same references.  This isn't a good situation.
  400. The first thing that the code for the COPYMESSAGE needs to do is to send itself an
  401. INITMESSAGE so that unique handles for the copy are created.  Once this is done, then
  402. the code for COPYMESSAGE can actually copy the data in these handles.  Once this final
  403. copying is done, we have a complete and separate copy of the original child.
  404.  
  405. UNDOMESSAGE:  When the user performs an undo or redo, involved objects get passed
  406. this message.  When an undo/redo occurs objects are either moving into or out of the
  407. document.  Objects move back and forth between the document and the undo hierarchy.
  408. The sub-messages UNDOFROMDOC and UNDOTODOC determine the direction.  There may be various
  409. tasks that need to be performed to this object and other parts of the document when an
  410. undo/redo occurs.  When an undo of a DELETE_EDIT is performed, it is typical to select
  411. the portion of the document undeleted.  The root object may hold the selection information.
  412. If this is the case, the object should call GetRootHndl() and then adjust the selection
  413. accordingly.  Note that the root object may turn out to be the undo hierarchy root object.
  414. If the object has been moved out of the document and into the undo hierarchy, this will
  415. turn out to be the case.  In this instance nothing would have to be done.  At least one
  416. message will be sent to the object while it is still in the document.  The sub-message
  417. will then state whether the document is about to leave or just was added to the document.
  418. Let's say that the root object contains a count of the number of selected items.  In this
  419. case, if the item is selected, and it is about to leave the document, then the count of
  420. items selected will need to be decremented.  If the item has just moved into the document,
  421. then the item needs to be set as selected, plus the number of selected items count needs
  422. to be increased by 1.
  423. An additional call is made prior to any objects being moved.  This is to globally prepare
  424. the document for an undo/redo operation.  Given that the items involved in the undo/redo
  425. should be selected, this suggests that other selected items should be deselected prior
  426. to the undo/redo.  This global undo/redo setup call is the place to do things like this.
  427. Also, once the undo/redo operation is complete and all objects involved in the operation
  428. are moved and messaged, a final call is made to clean up any unfinished undo/redo business.
  429. To recap the undo/redo procedure:
  430.     1) A global get-ready-to-undo/redo call is made.  This function is called UndoFixup.
  431.        It is passed a reference to the document, plus a sub-message stating that it is
  432.        called for pre-undo/redo tasks or post-undo/redo tasks (in this case pre-undo/redo).
  433.     2) Each object involved in the undo/redo task is called with appropriate messages
  434.        stating whether it is leaving or entering the document.  Appropriate document
  435.        maintenance tasks should be performed based on these messages/sub-messages.
  436.     1) UndoFixup is called a final time.  Once again, it is passed a reference to the document,
  437.        plus a sub-message stating that it is called for post-undo/redo tasks.
  438.  
  439. CONVERTMESSAGE:  This is called to convert handle references within an object to treeID
  440. values and visa versa, depending on the sub-message.
  441. For the sub-message CONVERTTOID:  Prior to the first object being written to disk, DoNumberTree()
  442. is called to assign a unique treeID to each object in the document.  For each handle reference
  443. in an object, call Hndl2ID() to convert it to a treeID value.  Once the object is written to
  444. disk, the object will be called again with the sub-message CONVERTTOHNDL.  This indicates that
  445. the handle references that were converted to treeID values should be converted back.  Call
  446. ID2Hndl() to do the reverse conversion.
  447. For the sub-message CONVERTTOHNDL:  The entire document is in memory prior to ever receiving this
  448. message.  In the case of writing a document to disk, the document is already in memory.  For
  449. the case where a document is being opened, the entire document is first read in, and then
  450. objects are passed this message as an opportunity to convert treeID values into handle references.
  451. In either case, DoNumberTree() will have already been called, so it is okay to call ID2Hndl().
  452.  
  453. FREADMESSAGE:  This is called to read in the data portion of an object.  The header
  454. information has already been read in.  Since the header information doesn't vary according
  455. to the object type, it can be read in generically.  Also, the header information states what
  456. type the object is, so until it the header is read in, the object type can't be determined.
  457. If the data doesn't have any additional handle references, just call the default function
  458. to read in the data.  The default function is called ReadTreeObjData().  It will read in
  459. the number of bytes designated by the dataSize in the header, which has already been read in.
  460. If there is additional data for the object to be kept in handles, or some such other unique
  461. situation, the code to do this goes here.
  462.  
  463. FWRITEMESSAGE:  This is called to write out the data portion of an object.  The header
  464. information has already been written.  Since the header information doesn't vary according
  465. to the object type, it can be written generically.  If the data doesn't have any additional
  466. handle references, just call the default function to write out the data.  The default
  467. function is called WriteTreeObjData().  It will write out the number of bytes designated by
  468. the dataSize in the header.  If there is additional data for the object kept in handles, or
  469. some such other unique situation, the code to write this additional data goes here.
  470.  
  471. HREADMESSAGE:  This message is similar to FREADMESSAGE, except that the data is read from
  472. a handle instead of from disk.  The assumption is that the data is already in a handle.
  473. A handle containing the object's data is passed in.  The handle is the same size as the dataSize
  474. field for the object.  If the object's data is flat, then you can simply do the following:
  475.     case HREADMESSAGE:
  476.         return(HReadTreeObjData(hndl, (Handle)data));
  477.         break;
  478. If the object's data isn't flat, then you will have to move the data from the handle into the
  479. object as is appropriate for this object.  The ability to stream data not only to the file,
  480. but to a handle is really convenient.  By calling the TreeObj.c function HWriteTree(), you can
  481. stream any tree (or branch) into a handle.  You can then save the handle as a resource, or you
  482. can send it to another application via AppleEvents.  Once read, it can be unstreamed by
  483. calling HReadTree().
  484.  
  485. HWRITEMESSAGE:  This message is similar to FWRITEMESSAGE, except that it is used to write
  486. the data into a handle.  See the HREADMESSAGE for more info.
  487.  
  488. HITTESTMESSAGE:  This message is used for hit-testing of an object, along with various
  489. targeting and keystroke information for the object.  See the DTS.Draw example for
  490. implementation details.
  491.  
  492. GETRGNMESSAGE:  This message is requesting that the object return a region that describes
  493. its shape.
  494.  
  495. GETBBOXMESSAGE:  This message is requesting that the object return a rectangle that encloses
  496. all portions of the object.
  497.  
  498. SETBBOXMESSAGE:  This message is used to change the size of the bounding rectangle for
  499. an object.
  500.  
  501. SECTBBOXMESSAGE:  This message is used to determine if the object's bounding rectangle
  502. intersects the given rectangle.
  503.  
  504. TARGETMESSAGE:  This message is how an object is made the target or not the target of keystrokes.
  505.  
  506. DRAWMESSAGE:  This message and sub-message is to tell the object to draw itself, and in
  507. what form.
  508.  
  509. PRINTMESSAGE:  This message is to tell the object to print itself.  Assumably the data
  510. field will vary, according to the object type.  Objects often print themselves differently
  511. than they draw, so specific object information will have to be passed in for these cases.
  512.  
  513. VHMESSAGE:  This message allows the object to format the data information that is viewed
  514. when using the View Hierarchy object viewing feature.
  515.  
  516.  
  517. As is evident from the above descriptions, the behaviors for the different types of objects
  518. is completely dependent on what is done for the various messages.  To define an object type,
  519. you need to make an entry into two tables.  These tables are:
  520.     1) gTreeObjMethods
  521.     2) gMinTreeObjSize
  522. These tables are found in the application file File.c.  As you add objects to your application,
  523. just make entries for these new objects into these tables.
  524. If you want the default behaviors for an object, then use nil for the method procedure.  If
  525. the method procedure is nil, then the object is passed no messages, as it is assumed that it
  526. is simple and generic enough to be handled automatically.  If you need to handle just one
  527. message specifically , you will then need to define a method procedure.  With method procedures,
  528. it is an all-or-nothing situation.  If you set the method procedure to non-nil for an object,
  529. then that object will receive all messages.  For these instances you can just call the default
  530. code directly, such as ReadTreeObjData() and WriteTreeObjData() for handling file I/O for
  531. the object.
  532.  
  533.  
  534. Since the object is a handle of three components, referencing the object as if it is unique
  535. can be a bit of a pain.  The unique portion of the object, the data portion, is actually in
  536. the middle of the object.  To syntactically fix this problem, you will want to define some
  537. macros for dereferencing into an object with appropriate object-type typecasting.  In
  538. FileFormat.h you will find some such dereferencing macros.  They are in the form:
  539.  
  540. #define mDerefRoot(hndl)     ((RootObj*)((*hndl) + 1))
  541.  
  542. This macro allows you to write code that looks like the following:
  543.  
  544.     numSelected = mDerefRoot(root)->numSelected;
  545.  
  546. The only danger with this is that the handle that is dereferenced doesn't have to be of type
  547. RootObj.  It can be of any type.  This is convenient and not.  It allows objects of different
  548. types to be treated similarly or differently, whichever the code demands.
  549.  
  550.  
  551. There are two defines in FileFormat.h that I should mention.  These are:
  552. 1) MAXNUMUNDOS
  553. 2) NUMSAVEUNDOS
  554.  
  555. These govern the depth of the undo mechanism.
  556.  
  557. MAXNUMUNDOS is the maximum number of undos that will be recorded before the oldest are
  558. automatically purged.  This can be set up to 65535, if you so wish, although that will
  559. cause a lot of undo hierarchy object to be kept around and is more than any human user
  560. can comprehend.
  561.  
  562. NUMSAVEUNDOS is the maximum number of undos that are saved along with the document.  If this
  563. number if non-zero, then when the document is opened, the user may already have some undos
  564. that can be performed from the last editing session.  Setting this constant to zero makes
  565. the application save no undos along with the document.
  566.  
  567.  
  568. It is generally better if your object's data area is flat.  This is good for a number of reasons:
  569.  
  570. 1)    It allows you to use the default streaming functions for FREADMESSAGE, FWRITEMESSAGE,
  571.     HREADMESSAGE, HWRITEMESSAGE.
  572. 2)    Your application uses fewer handles.  The more handles, the slower the memory manager gets.
  573.  
  574. Of course, for some object, being completely flat is unreasonable.  The framework supports both flat
  575. and non-flat objects, so the choice is yours.
  576.  
  577.  
  578. To make keeping the object's data flat easier, I've added the following functions:
  579.  
  580. long            GetCData(TreeObjHndl hndl, long offset, char *data);
  581. OSErr            PutCData(TreeObjHndl hndl, long offset, char *data);
  582. void            GetPData(TreeObjHndl hndl, long offset, StringPtr data);
  583. OSErr            PutPData(TreeObjHndl hndl, long offset, StringPtr data);
  584. OSErr            PutShortData(TreeObjHndl hndl, long offset, void *data, unsigned short size);
  585. OSErr            PutLongData(TreeObjHndl hndl, long offset, void *data, long size);
  586. unsigned long    GetDataOffset(TreeObjHndl hndl, unsigned long offset, short dtype, short dnum);
  587.  
  588. GetDataOffset is the key call.  It allows you to calculate an offset into the objects data,
  589. even if the data in front of it is of variable size.  Let's takle a look at an object that is
  590. quite variable.  You will find the following typedef in the file DTS.Lib.h:
  591.  
  592. typedef struct {
  593.     short    numRows;
  594.     short    numCols;
  595.     short    cellHeight;
  596.     short    cellWidth;
  597.     short    mode;
  598. } CLNewInfo;
  599. typedef struct {
  600.     Rect    destRct;
  601.     Rect    viewRct;
  602.     Rect    brdrRct;
  603.     short    maxTextLen;
  604.     short    mode;
  605. } CTENewInfo;
  606. typedef struct {
  607.     short            selected;
  608.     Rect            rect;
  609.     char            visible;
  610.     char            hilite;
  611.     short            value, min, max;
  612.     short            procID;
  613.     long            refCon;
  614.     short            ctlID;
  615.     short            cctbID;
  616.     short            fontSize;
  617.     Style            fontStyle;
  618.     union {
  619.         CLNewInfo    clnew;
  620.         CTENewInfo    ctenew;
  621.     } extCtl;
  622.     unsigned char    title[4];        /* 4 pascal strings are stored back-to-back starting here. */
  623.                                     /* title[0] = control title */
  624.                                     /* title[1] = key equivs    */
  625.                                     /* title[2] = control font  */
  626.                                     /* title[3] = balloon help info */
  627.     short            textLen[2];        /* 2 short-prefixed data blocks stored starting here.
  628.                                     ** textLen[0] = textEdit control text block
  629.                                     ** textLen[1] = textEdit control style block */
  630. } CtlObj;
  631. #define mDerefCtl(hndl) ((CtlObj*)((*hndl) + 1))
  632.  
  633. Everything is reasonable until we get to the textLen field.  If we were to get the offset
  634. of this field using the offsetof macro (found in Utilities.h), we would get a value that is
  635. 4 bigger than the offset for the title field.  This is initially true if the struct is filled
  636. with zeros.  (We would then have 4 zero-length pascal strings in front on textLen).  Once we
  637. modify one of these title strings, then the textLen data would slide down.
  638.  
  639. To get the correct offset for textLen[1], we would do the following:
  640.  
  641. ofst = GetDataOffset(hndl, offsetof(CtlObj,title), kPStr, 4);
  642.         /* This gets us past the 4 pascal strings. */
  643.  
  644. ofst = GetDataOffset(hndl, ofst, kSDataBlock, 1);
  645.         /* This gets us past the first short block of data.  A short block of data is a 2-byte
  646.         ** data length, followed by the data.  Note that even though textLen is a signed short,
  647.         ** GetDataOffset treats it as unsigned.  This allows us to be a bit more descriptive
  648.         ** in our typedef.  By stating that it is a short, we are indicating that we don't
  649.         ** expect the data to get bigger than 32767. */
  650.  
  651. Now that we have this offset, we can use the PutShortData() call to replace this data.
  652. Let's assume that we have a handle of text called txtHndl, and the length is txtLen.  To replace
  653. the old block of data with this text, we would make the following call:
  654.  
  655.     err = PutShortData(hndl, ofst, txtHndl, txtLen);
  656.  
  657. Any data fields that are after this field are moved to adjust for the size differences between
  658. the data being replaced and the new data.
  659.  
  660. Long data blocks are also supported, as well as C and pascal string data types.
  661. There are also Get functions for the string types, but there aren't for the block types.
  662. The assumption is that you will need to create a handle to copy the data into.  To get a
  663. data block, first generate the offset, then get the short or long for the data size.  Create
  664. a handle of this size, and then BlockMove the data.
  665.  
  666. To get the data for this short block, the code would look like this:
  667.  
  668.     short    ofst;
  669.     Ptr        dptr;
  670.  
  671.     ofst = GetDataOffset(hndl, offsetof(CtlObj,title), kPStr, 4);
  672.     ofst = GetDataOffset(hndl, ofst, kSDataBlock, 1);
  673.     dptr = GetDataPtr(hndl);
  674.     BlockMove(dptr + ofst, &dsiz, sizeof(short));
  675.     dhndl = NewHandle(dsiz);
  676.     if (dhndl) {
  677.         dptr = GetDataPtr(hndl);
  678.         BlockMove(dptr + ofst, *dhndl, dsiz);
  679.     }
  680.  
  681. Not too bad for fetching a variable-length field from a variable location.
  682.  
  683. Of course, for strings, it's a snap.  Here's the code to get title[2]:
  684.  
  685.     short    ofst;
  686.     Str255    pstr;
  687.  
  688.     ofst = GetDataOffset(hndl, offsetof(CtlObj,title), kPStr, 2);
  689.     GetPData(hndl, ofst, pstr);
  690.  
  691.  
  692.  
  693. The final thing I will cover here is the 'View Hierarchy' facility in this sample.  As
  694. powerful as a hierarchical document structure can be, it can also be difficult to examine
  695. and debug.  The debugging feature of a 'View Hierarchy' window has been introduced to
  696. address this problem.
  697.  
  698. Whatever window is the top-most window when 'View Hierarchy' is selected will be referenced
  699. by the 'View Hierarchy' window.  The 'View Hierarchy' window will be given the same window
  700. name as the window being referenced to keep confusion to a minimum if multiple 'View Hierarchy'
  701. windows are opened at once.
  702.  
  703. The TextEdit control on the left is used to display the data in the object.  The small TextEdit
  704. control near the top-right is used to enter an alternate object handle to display.  The List
  705. control on the left displays the parent hierarchy.  The List control on the right is used
  706. to display the child handle table of the currently displayed object.  By double-clicking on
  707. members of these two lists, you can navigate the document hierarchy.
  708.  
  709. The data TextEdit control displays the header information of the object, plus the data portion
  710. of the object.
  711.  
  712. For root objects, the first 4 bytes of the data area hold a reference to the undo hierarchy of
  713. the document.  To view the undo hierarchy, either enter this handle in the small TextEdit
  714. control and press display, or just select the text for the handle in the data display area and
  715. press display.  Either method will make the undo root object the object to display.
  716.  
  717. The undo root object holds the document root in the first 4 bytes of the data area, so to
  718. go back to displaying the document root, just do the same as you did to display the undo root
  719. object.
  720.  
  721. If you try to display a handle that doesn't exist, the 'View Hierarchy' code will beep at you
  722. and do nothing.  If you enter a handle that exists, but isn't an object handle, it will try
  723. to display something and probably crash.  (Remember, this is only meant as a debugging aid.)
  724.  
  725.  
  726. This is probably enough for an introduction.  Have fun.
  727.  
  728.